import { GetPoseCommand, LookAtScreenCoordsCommand, MoveDirectionCommand, PanCommand, RotateCommand, SetOrientationCommand } from "../command/camera.command"
import SdkCommand from "../command/sdk.command"
import cameraEnum from "../enum/camera.enum"
import directionEnum from "../enum/direction.enum"
import modeEnum from "../enum/mode.enum"
import Logger from "../utils/Logger"
import { AggregateSubscription, SdkObservable } from "./observable"
import { BaseException, ClassFactory, copyPosition, copyXY, DeprecatedDecorator, isSame, isSamePoint, ScreenCapturer, ScreenshotExecutor } from "./utils"

class ArraySizeMismatchException extends BaseException {
  constructor() {
    super("Cannot copy into an array of a different size")
    this.name = "ArraySizeMismatch"
  }
}

class PoseSubFactory {
  create(e, t, o, n) {
    const r = () => e.onChanged()
    return new AggregateSubscription(t.onChanged(r), o.onChanged(r), n.onChanged(r))
  }
}

class ObservablePose {
  _data: { position: { x: number; y: number; z: number }; rotation: { x: number; y: number }; projection: Float32Array; sweep: string; mode: any }
  THREE: any
  viewmodeConverter: any
  tempEuler: any
  constructor(THREE, o) {
    this._data = {
      position: { x: 0, y: 0, z: 0 },
      rotation: { x: 0, y: 0 },
      projection: new Float32Array(16),
      sweep: "",
      mode: THREE.TRANSITIONING
    }
    this.THREE = THREE
    this.viewmodeConverter = o
    this.tempEuler = new THREE.Euler()
  }
  get data() {
    return this._data
  }
  equals(e) {
    const t = 0.00001
    return (
      isSamePoint(e._data.position, this._data.position, t) &&
      isSamePoint(e._data.rotation, this._data.rotation, t) &&
      ((e, t, o = 0.00001) => e.length === t.length && !Array.prototype.some.call(e, (e, n) => !isSame(e, t[n], o)))(
        e._data.projection,
        this._data.projection,
        t
      ) &&
      e._data.sweep === this._data.sweep &&
      e._data.mode === this._data.mode
    )
  }
  update(e, t, o, n) {
    const r = this.tempEuler.setFromQuaternion(e.pose.rotation, "YXZ")
    const a = t.currentSweep && t.isSweepUnaligned(t.currentSweep)
    copyPosition(this._data.position, e.pose.position)
    this._data.rotation.x = this.THREE.MathUtils.radToDeg(r.x)
    this._data.rotation.y = this.THREE.MathUtils.radToDeg(r.y)
    this._data.projection = Float32Array.from(e.pose.projection.asThreeMatrix4().transpose().elements)
    this._data.sweep = t.currentSweepObject ? n.getIdForSweep(t.currentSweepObject) : ""
    this._data.mode = this.viewmodeConverter.toSdk(o.currentMode, !a)
  }
  copy(e) {
    copyPosition(this._data.position, e.data.position)
    copyXY(this._data.rotation, e.data.rotation)
    if (this._data.projection.byteLength !== e.data.projection.byteLength) throw new ArraySizeMismatchException()
    const o = e.data.projection.length
    for (let n = 0; n < o; ++n) this._data.projection[n] = e.data.projection[n]
    this._data.sweep = e.data.sweep
    this._data.mode = e.data.mode
  }
}
const allowZoom = function (val, minZoom, maxZoom) {
  return Math.max(minZoom, Math.min(val, maxZoom))
}
class ZoomToExecutor {
  Viewmode: any
  ZoomSetCommand: any
  issueCommand: any
  constructor(e, t) {
    this.Viewmode = e.Viewmode
    this.ZoomSetCommand = e.ZoomSetCommand
    this.issueCommand = t.issueCommand
  }
  validateInput(e) {
    const minZoom = 0.7
    const maxZoom = 3
    const log = new Logger("command.zoom")
    const t = e.zoomPct - 0
    if (isNaN(t)) throw Error(t + " is not a valid zoom level")
    t < minZoom && maxZoom < t && ((e.zoomPct = allowZoom(t, minZoom, maxZoom)), log.warn(t, `is outside the valid zoom range of [${minZoom}, ${maxZoom}]`))
    return e
  }
  async exec(e, t, o, n) {
    checkMode(this.Viewmode, n.currentMode), await this.issueCommand(new this.ZoomSetCommand(e.zoomPct)), t.return(o.zoom())
  }
}
class ZoomByExecutor {
  Viewmode: any
  ZoomInCommand: any
  issueCommand: any
  constructor(e, t) {
    this.Viewmode = e.Viewmode
    this.ZoomInCommand = e.ZoomInCommand
    this.issueCommand = t.issueCommand
  }
  validateInput(e) {
    const t = e.zoomDelta - 0
    if (isNaN(t)) throw Error(t + " is not a valid zoom delta")
    return (e.zoomDelta = t || 0), e
  }
  async exec(e, t, o, n) {
    checkMode(this.Viewmode, n.currentMode), await this.issueCommand(new this.ZoomInCommand(e.zoomDelta)), t.return(o.zoom())
  }
}
class ZoomResetExecutor {
  Viewmode: any
  ZoomResetCommand: any
  issueCommand: any
  constructor(e, t) {
    this.Viewmode = e.Viewmode
    this.ZoomResetCommand = e.ZoomResetCommand
    this.issueCommand = t.issueCommand
  }
  validateInput(e) {
    return e
  }
  async exec(e, t, o, n) {
    checkMode(this.Viewmode, n.currentMode), await this.issueCommand(new this.ZoomResetCommand()), t.return()
  }
}
function checkMode(e, t) {
  if (t !== e.Panorama) throw Error("Zoom controls are currently only supported in Panorama mode")
}
class ZoomSubFactory {
  create(e, t) {
    return t.onChanged(() => e.onChanged())
  }
}
class ZoomObservableData {
  _data: { level: number }
  constructor(e) {
    this._data = { level: 1 }
  }
  get data() {
    return this._data
  }
  equals(e) {
    return this._data.level === e.data.level
  }
  copy(e) {
    this._data.level = e.data.level
  }
  update(e, t) {
    t.isInside() && (this._data.level = e.zoom())
  }
}
export default {
  Direction: sdk => sdk.addEnumToInterface({ namespace: "Camera", name: "Direction", values: directionEnum }),
  Event: sdk => sdk.addEnumToInterface({ namespace: "Camera", name: "Event", values: cameraEnum }),
  getPose: (sdk, allFunc, dependencies) => {
    dependencies.getDependencies().then(([e, n, r, a]) => {
      const s = {
        position: { x: 0, y: 0, z: 0 },
        rotation: { x: 0, y: 0 },
        projection: new Float32Array(16),
        sweep: "",
        mode: modeEnum.TRANSITIONING
      }
      allFunc.addBinding(GetPoseCommand, async () => allFunc.getPose(s, e, n, r, a))
    })
    sdk.addCommandCreator({ namespace: "Camera", name: "getPose", args: [] }, () => new GetPoseCommand())
  },
  pan: (sdk, THREE, allFunc, dependencies) => {
    dependencies.getDependencies().then(([e, n]) => {
      allFunc.addBinding(PanCommand, async o => {
        const r = n.pose.position,
          a = o.x - r.x,
          s = o.z - r.z,
          i = new THREE.Vector2(a, s),
          d = i.length()
        i.setLength(0.005), await e.startTranslateTransition(d / 0.005, i, !1)
      })
    })
    sdk.addCommandCreator({ namespace: "Camera", name: "pan", args: ["position"] }, e => {
      if (
        (((e = e || {}).position = e.position || {}),
        (e.position.x = e.position.x - 0),
        (e.position.z = e.position.z - 0),
        isNaN(e.position.x) || isNaN(e.position.z))
      )
        throw new Error(`${JSON.stringify(e)} does not contain a valid position to pan`)
      return new PanCommand({ x: e.position.x, z: e.position.z })
    })
  },
  pose: (sdk, THREE, allFunc, dependencies) => {
    const observable = SdkObservable.create(dependencies, new PoseSubFactory(), new ClassFactory(ObservablePose, THREE, allFunc.viewmodeConverter))
    sdk.addObservableToInterface({ namespace: "Camera", name: "pose" }, observable)
  },
  rotate: (sdk, THREE, allFunc, dependencies) => {
    const r = THREE.MathUtils.degToRad(80) / 1000
    dependencies.getDependencies().then(([e, t, n]) => {
      allFunc.addBinding(RotateCommand, async t => await allFunc.rotateCamera(e, t))
    })
    const a = e => {
      const o = (e = e || {}).options || {},
        n = THREE.MathUtils.degToRad(e.xAngle || 0),
        a = THREE.MathUtils.degToRad(e.yAngle || 0),
        s = THREE.MathUtils.degToRad(e.zAngle || 0)
      let i = r
      if (o.speed) {
        if (isNaN(o.speed) || o.speed <= 0) throw new Error(`${JSON.stringify(o)} does not contain valid rotation speed`)
        i = THREE.MathUtils.degToRad(o.speed) / 1000
      }
      if (isNaN(n) || isNaN(a) || isNaN(s)) throw new Error(`${JSON.stringify(e)} does not contain valid rotation angles`)
      return { xAngle: n, yAngle: a, zAngle: s, rotationSpeed: i }
    }
    sdk.addCommandCreator({ namespace: "Camera", name: "rotate", args: ["xAngle", "yAngle", "options"] }, e => new RotateCommand(a(e)))
  },
  setRotation: (sdk, THREE, allFunc, dependencies) => {
    const r = THREE.MathUtils.degToRad(80) / 1000
    dependencies.getDependencies().then(([e, t, n]) => {
      allFunc.addBinding(SetOrientationCommand, async r => await allFunc.orientCamera(t, e, n, r))
    })
    const a = e => {
      const o = (e = e || {}).options || {},
        n = THREE.MathUtils.degToRad(e.xAngle || 0),
        a = THREE.MathUtils.degToRad(e.yAngle || 0),
        s = THREE.MathUtils.degToRad(e.zAngle || 0)
      let i = r
      if (o.speed) {
        if (isNaN(o.speed) || o.speed <= 0) throw new Error(`${JSON.stringify(o)} does not contain valid rotation speed`)
        i = THREE.MathUtils.degToRad(o.speed) / 1000
      }
      if (isNaN(n) || isNaN(a) || isNaN(s)) throw new Error(`${JSON.stringify(e)} does not contain valid rotation angles`)
      return { xAngle: n, yAngle: a, zAngle: s, rotationSpeed: i }
    }

    sdk.addCommandCreator({ namespace: "Camera", name: "setRotation", args: ["rotation", "options"] }, e => {
      const t = (e = e || {}).rotation || {}
      return new SetOrientationCommand(a({ xAngle: t.x, yAngle: t.y, zAngle: t.z, options: e.options }))
    })
  },
  lookAtScreenCoords: (sdk, THREE, allFunc, dependencies) => {
    dependencies.getDependencies().then(([e, n, r]) => {
      allFunc.addBinding(LookAtScreenCoordsCommand, async (a: any = {}) => {
        if (!r.isInside()) throw Error("Camera.lookAtScreenCoords must be called from Inside mode")
        const s = allFunc.screenPositionToNDC(a.x, a.y, n.width, n.height),
          i = new THREE.Vector3(0, 0, -1).applyQuaternion(n.pose.rotation),
          d = allFunc.ndcToWorldPosition(n, new THREE.Vector2(s.x, s.y), 1).normalize(),
          c = i.clone().setY(0).angleTo(d.clone().setY(0)),
          u = Math.asin(d.y - i.y),
          p = c * Math.sign(s.x),
          l = u
        return allFunc.rotateCamera(e, { xAngle: p, yAngle: l, zAngle: 0, rotationSpeed: (40 * Math.PI) / 180 / 1000 })
      })
    })
    sdk.addCommandCreator({ namespace: "Camera", name: "lookAtScreenCoords", args: ["x", "y"] }, (e: any = {}) => {
      if (((e.x = e.x || 0), (e.y = e.y || 0), isNaN(e.x) || isNaN(e.y))) throw new Error(`${JSON.stringify(e)} does not contain valid screen coordinates`)
      return new LookAtScreenCoordsCommand({ x: e.x, y: e.y })
    })
  },
  moveInDirection: (sdk, allFunc, dependencies) => {
    dependencies.getDependencies().then(([t]) =>
      allFunc.addBinding(MoveDirectionCommand, async n => {
        const direction = n.direction
        if (!Object.values(directionEnum).includes(n.direction)) throw new Error(`${n.direction} is not a valid direction`)
        await t.navigateInLocalDirection(allFunc.directionConverter.toVector(direction))
      })
    )
    sdk.addCommandCreator({ namespace: "Camera", name: "moveInDirection", args: ["direction"] }, e => new MoveDirectionCommand(e || {}))
  },
  zoomTo: (sdk, exportOBJ, allFunc, dependencies) => {
    const command = SdkCommand.create(dependencies, new ClassFactory(ZoomToExecutor, exportOBJ, allFunc))
    sdk.addCommandToInterface({ namespace: "Camera", name: "zoomTo", args: ["zoomPct"] }, command)
  },
  zoomBy: (sdk, exportOBJ, allFunc, dependencies) => {
    const command = SdkCommand.create(dependencies, new ClassFactory(ZoomByExecutor, exportOBJ, allFunc))
    sdk.addCommandToInterface({ namespace: "Camera", name: "zoomBy", args: ["zoomDelta"] }, command)
  },
  zoomReset: (sdk, exportOBJ, allFunc, dependencies) => {
    const command = SdkCommand.create(dependencies, new ClassFactory(ZoomResetExecutor, exportOBJ, allFunc))
    sdk.addCommandToInterface({ namespace: "Camera", name: "zoomReset", args: [] }, command)
  },
  zoom: (sdk, exportOBJ, dependencies) => {
    const observable = SdkObservable.create(dependencies, new ZoomSubFactory(), new ClassFactory(ZoomObservableData, exportOBJ))
    sdk.addObservableToInterface({ namespace: "Camera", name: "zoom" }, observable)
  },
  takeScreenShot: (sdk, THREE, exportOBJ, allFunc, dependencies) => {
    const a = new ScreenCapturer(new THREE.Camera(), allFunc.encodeRenderTarget),
      s = new ClassFactory(ScreenshotExecutor, a, exportOBJ, allFunc),
      d = SdkCommand.create(dependencies, new ClassFactory(DeprecatedDecorator, s, "Camera.takeScreenshot deprecated, please use Renderer.takeScreenshot"))
    sdk.addCommandToInterface({ namespace: "Camera", name: "takeScreenShot", args: ["resolution", "visibleObjects", "returnType"] }, d)
  },
  onEvent: (sdk, allFunc, dependencies) => {
    dependencies.getDependencies().then(([n, r, a, s]) => {
      const i = [
        {
          position: { x: 0, y: 0, z: 0 },
          rotation: { x: 0, y: 0 },
          projection: new Float32Array(16),
          sweep: "",
          mode: modeEnum.TRANSITIONING
        }
      ]
      const d = (() => {
        const log = new Logger("broadcast.move")
        let t = 0,
          c = !1
        const u = () => (allFunc.getPose(i[0], n, r, a, s), i)
        return () => {
          const o = Date.now(),
            n = t + 100
          if (o > n) {
            c = !1
            t = o
            try {
              sdk.broadcast(cameraEnum.MOVE, u)
            } catch (e) {
              log.debug("failed to broadcast pose, one of the module dependencies are probably not loaded yet")
            }
          } else c || (setTimeout(() => d(), n - o), (c = !0))
        }
      })()
      n.onChanged(d), a.onChanged(d)
    })
  }
}
